package com.jfinal.plugin.sqlxml;

import com.jfinal.log.Logger;
import com.jfinal.plugin.activerecord.Model;
import com.jfinal.plugin.activerecord.Page;
import com.jfinal.plugin.activerecord.Record;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * 好吧 用来构建方法的类
 *
 * @author tangyang
 */
public class MethodCreator {

    static final Logger logger = Logger.getLogger(MethodCreator.class);

    static enum mtype {

        LIST_MODEL, LIST_RECORD,
        PAGE_MODEL, PAGE_RECORD,
        QUERY_DOUBLE("queryDouble"), QUERY_INT("queryInt"),
        QUERY_LONG("queryLong"),
        QUERY_FLOAT("queryFloat"), QUERY_BIGINTEGER("queryBigInteger"),
        QUERY_TIMESTAMP("queryTimstamp"), QUERY_DATE("queryDate"),
        QUERY_BOOLEAN("queryBoolean"), QUERY_BYTES("queryBytes"),
        QUERY_BIGDECIMAL("queryBigDecimal"), QUERY_STRING("queryString"),
        FIND_ONE,
        QUERY_RECORD, UPDATE;

        String JFinalQueryName;

        private mtype() {
        }

        private mtype(String JFinalQueryName) {
            this.JFinalQueryName = JFinalQueryName;
        }

    }
    CtClass currentInterface;
    Class orignalInterface;
    CtClass theClass;
    Method[] methods;
    Node baseNode;
    Method iMethod = null;

    Class<?>[] parameterTypes;
    Class<?>[] exceptionTypes;

    StringBuilder sqlCodeBody = new StringBuilder();

    StringBuilder codeBody = new StringBuilder();

    Class modelType;

    static final String DEFAULT_ARG_NAME_PRI = "arg";

    public MethodCreator(
            CtClass currentInterface,
            Class orignalInterface,
            CtClass theClass,
            Method[] methods,
            Node node) {

        this.currentInterface = currentInterface;
        this.orignalInterface = orignalInterface;
        this.theClass = theClass;
        this.methods = methods;
        this.baseNode = node;
    }

    void startCreateMethod(mtype type) {

        Node actionNode = baseNode.getAttributes().getNamedItem(XmlDaoBeanCreator.ATTR_ACTION);
        if (actionNode == null) {
            throw new SqlXmlException("You must define the action in your node;");
        }
        String action = actionNode.getTextContent().trim();
        NodeList nodeList = baseNode.getChildNodes();

        createSqlCodeBody(nodeList);

        for (Method need : methods) {

            if (need.getName().equals(action)) {

                iMethod = need;

            }

        }
        if (iMethod == null) {
            return;
        }
        if (SqlXmlPlugin.debug) {
            logger.info("start create method for action " + action);
        }
        Class<?> returnType = iMethod.getReturnType();

        boolean hasActualType = true;

        Type[] actualTypeArguments = getActualTypeArguments(iMethod);

        if (actualTypeArguments == null) {

            hasActualType = false;

        }

        if (List.class.isAssignableFrom(returnType)) {
            if (hasActualType) {
                modelType = (Class) actualTypeArguments[0];
                if (Model.class.isAssignableFrom(modelType)) {
                    createMethodByCode(mtype.LIST_MODEL);
                } else if (Record.class.isAssignableFrom(modelType)) {
                    createMethodByCode(mtype.LIST_RECORD);
                } else {
                    throw new SqlXmlException("sorry,your cannot just use a javabean.");
                }
                return;
            }
        }
        if (Page.class.isAssignableFrom(returnType)) {
            if (hasActualType) {
                modelType = (Class) actualTypeArguments[0];
                if (Model.class.isAssignableFrom(modelType)) {
                    createMethodByCode(mtype.PAGE_MODEL);
                } else if (Record.class.isAssignableFrom(modelType)) {
                    createMethodByCode(mtype.PAGE_RECORD);
                } else {
                    throw new SqlXmlException("sorry,your cannot just use a javabean.");
                }
                return;

            }
        }
        if (Model.class.isAssignableFrom(returnType)) {
            createMethodByCode(mtype.FIND_ONE);
            return;
        }
        if (returnType == int.class || returnType == Integer.class) {
            if (type != null && type == mtype.UPDATE) {
                createMethodByCode(type);
            } else {
                createMethodByCode(mtype.QUERY_INT);
            }
            return;

        }
        if (returnType == double.class || returnType == Double.class) {
            createMethodByCode(mtype.QUERY_DOUBLE);
            return;

        }
        if (returnType == float.class || returnType == Float.class) {
            createMethodByCode(mtype.QUERY_FLOAT);
            return;

        }
        if (returnType == BigDecimal.class) {
            createMethodByCode(mtype.QUERY_BIGDECIMAL);
            return;

        }
        if (returnType == long.class || returnType == Long.class) {
            createMethodByCode(mtype.QUERY_LONG);
            return;

        }
        if (Timestamp.class.isAssignableFrom(returnType)) {
            createMethodByCode(mtype.QUERY_TIMESTAMP);
            return;

        }
        if (Date.class.isAssignableFrom(returnType)) {
            createMethodByCode(mtype.QUERY_DATE);
            return;

        }
        if (String.class.isAssignableFrom(returnType)) {
            createMethodByCode(mtype.QUERY_STRING);
            return;

        }
        if (Boolean.class.isAssignableFrom(returnType) || returnType == boolean.class) {
            if (type != null && type == mtype.UPDATE) {
                createMethodByCode(type);
            } else {
                createMethodByCode(mtype.QUERY_BOOLEAN);
            }
            return;

        }
        if (byte[].class == returnType || Byte[].class == returnType) {
            createMethodByCode(mtype.QUERY_BYTES);
            return;

        }

        if (BigInteger.class == returnType) {
            createMethodByCode(mtype.QUERY_BIGINTEGER);
            return;
        }
        if (Record.class == returnType) {
            createMethodByCode(mtype.QUERY_RECORD);
            return;
        }

    }

    void startCreateMethod() {
        startCreateMethod(null);
    }

    private String[] getArgsName() {

        try {
            CtMethod cm = null;
            CtMethod[] declaredMethods = currentInterface.getDeclaredMethods();
            for (CtMethod declaredMethod : declaredMethods) {
                if (declaredMethod.getName().equals(iMethod.getName())) {
                    cm = declaredMethod;
                    break;
                }
            }
            if (cm == null) {
                throw new SqlXmlException("I don`t know what happened!");
            }
            String[] paramNames = new String[cm.getParameterTypes().length];
            MethodInfo methodInfo = cm.getMethodInfo();
            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
            LocalVariableAttribute attr = null;
            if (codeAttribute != null) {
                attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
            }
            if (attr != null) {
                int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
                for (int i = 0; i < paramNames.length; i++) {
                    paramNames[i] = attr.variableName(i + pos);
                    // paramNames即参数名  
                }
            } else {
                for (int i = 0; i < paramNames.length; i++) {
                    paramNames[i] = DEFAULT_ARG_NAME_PRI + i;
                }
            }
            Annotation[][] parameterAnnotations = iMethod.getParameterAnnotations();
            for (int i = 0; i < parameterAnnotations.length; i++) {
                Annotation[] aa = parameterAnnotations[i];
                if (aa.length > 0) {
                    paramNames[i] = ((Para) aa[0]).name();
                }
            }
            Node parasNode = baseNode.getAttributes().getNamedItem(XmlDaoBeanCreator.ATTR_PARAS);
            if (parasNode != null) {
                String[] paraNs = parasNode.getTextContent().trim().split(",");
                for (int i = 0; i < paramNames.length; i++) {
                    paramNames[i] = paraNs[i].trim();
                }
            }
            return paramNames;
        } catch (NotFoundException ex) {
            throw new SqlXmlException("I don`t know what happened!");
        }
    }

    private Type[] getActualTypeArguments(Method iMethod) {
        Type returnType = iMethod.getGenericReturnType();
        if (returnType instanceof Class<?>) {
            return null;
        } else if (returnType instanceof ParameterizedType) {
            return ((ParameterizedType) returnType).getActualTypeArguments();

        }
        return null;
    }

    private void createSqlCodeBody(NodeList nodeList) {
        int childNodesLen = nodeList.getLength();
        sqlCodeBody.append("StringBuilder sql0=new StringBuilder();");
        sqlCodeBody.append("java.util.ArrayList " + XmlDaoBeanCreator.SQL_OBJ_LIST_STR + "=new java.util.ArrayList();");

        int currentSqlBuilder = 0;
        for (int i = 0; i < childNodesLen; i++) {
            Node item = nodeList.item(i);
            short type = item.getNodeType();
            switch (type) {
                case Node.TEXT_NODE:

                    buildSqlText(currentSqlBuilder, item.getNodeValue());
                    break;
                case Node.ELEMENT_NODE:
                    String nodeName = item.getNodeName();
                    if (XmlDaoBeanCreator.TAG_USE_PAGE.equals(nodeName)) {
                        currentSqlBuilder = 1;
                        sqlCodeBody.append("StringBuilder sql1=new StringBuilder();");
                        buildPage();
                    } else if (XmlDaoBeanCreator.TAG_IF.endsWith(nodeName)) {
                        buildIf(item, currentSqlBuilder);
                    }
                    break;
            }
        }
    }

    private void buildPage() {

    }

    private void buildSqlText(int currentSqlBuilder, String text) {
        changeSqlTextToRealSql(currentSqlBuilder, text);
    }

    private void buildIf(Node currentItem, int currentSqlBuilder) {
        String sqlContext = currentItem.getTextContent();
        String condition = currentItem.getAttributes().getNamedItem("condition").getNodeValue();
        sqlCodeBody.append("if(").append(condition).append(")").append("{");
        boolean isException = false;
        String exceptionInfo = null;
        NodeList nodes = currentItem.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node item = nodes.item(i);
            if (item.getNodeName().equals(XmlDaoBeanCreator.TAG_EXCEPTION)) {
                exceptionInfo = item.getTextContent();
                isException = true;
            }
        }
        if (isException) {
            sqlCodeBody.append("throw new  com.jfinal.plugin.sqlxml.SqlXmlException(\"").append(removeSpecialChars(exceptionInfo)).append("\");");
        } else {

            changeSqlTextToRealSql(currentSqlBuilder, sqlContext);

        }
        sqlCodeBody.append("}");
    }

    private void createMethodByCode(mtype type) {

        parameterTypes = iMethod.getParameterTypes();
        exceptionTypes = iMethod.getExceptionTypes();
        createMethodHeadInfoCode(parameterTypes, exceptionTypes);
        switch (type) {
            case LIST_RECORD:
                writeListRecordTypeCode();
                break;
            case LIST_MODEL:
                writeListModelTypeCode();
                break;
            case PAGE_RECORD:
                writePageRecordTypeCode();
                break;
            case PAGE_MODEL:
                writePageModelTypeCode();
                break;
            case QUERY_DOUBLE:;
            case QUERY_INT:;
            case QUERY_LONG:;
            case QUERY_FLOAT:;
            case QUERY_BIGINTEGER:;
            case QUERY_TIMESTAMP:;
            case QUERY_DATE:;
            case QUERY_BOOLEAN:;
            case QUERY_BYTES:;
            case QUERY_BIGDECIMAL:;
            case QUERY_STRING:
                writeQueryTypeCode(type);
                ;
                break;
            case FIND_ONE:
                writeFindOneTypeCode();
                break;
            case QUERY_RECORD:
                writeQueryRecordTypeCode();
                break;
            case UPDATE:
                writeUpdateTypeCode();
                break;
        }
        addMethod(codeBody.toString());

    }

    private void createMethodHeadInfoCode(Class<?>[] parameterTypes, Class<?>[] exceptionTypes) {
        String[] argsName = getArgsName();
        codeBody.append("public ")
                .append(iMethod.getReturnType().getName())
                .append(" ")
                .append(iMethod.getName())
                .append(" (");
        if (parameterTypes.length > 0) {
            for (int i = 0; i < parameterTypes.length; i++) {
                codeBody.append(parameterTypes[i].getTypeName()).append(" ").append(argsName[i]);
                if (i != parameterTypes.length - 1) {
                    codeBody.append(",");
                }
            }
        }
        codeBody.append(")");
        if (exceptionTypes.length > 0) {
            codeBody.append("throws ");
            for (int i = 0; i < exceptionTypes.length; i++) {
                codeBody.append(exceptionTypes[0].getTypeName());
                if (i != exceptionTypes.length - 1) {
                    codeBody.append(",");
                }
            }
        }
    }

    private CtField createOrGetFiled(String daoFieldName) {
        CtField daoField = null;
        try {
            daoField = theClass.getField(daoFieldName);
        } catch (NotFoundException ex) {
            daoField = null;
        }
        if (daoField == null) {
            try {
                daoField = CtField.make("private " + modelType.getName() + " " + daoFieldName + "=new " + modelType.getName() + "();", theClass);
                theClass.addField(daoField);
            } catch (CannotCompileException ex) {
                throw new SqlXmlException("sorry,I donnot know what happen.Maybe your Model does not have a no parameter constructor.", ex);
            }
        }
        return daoField;
    }

    private void changeSqlTextToRealSql(int currentSqlBuilder, String sql) {
        Matcher matcher = XmlDaoBeanCreator.allPattern.matcher(sql);
        int lastEnd = 0;
        while (matcher.find()) {

            if (matcher.group().charAt(0) == '#') {
                matcher.start();
                sqlCodeBody.append("sql").append(currentSqlBuilder)
                        .append(".append(\"")
                        .append(removeSpecialChars(sql.substring(lastEnd, matcher.start())))
                        .append("\").append(").append(matcher.group(2)).append(");");
                lastEnd = matcher.end();
            } else if (matcher.group().charAt(0) == '$') {
                sqlCodeBody.append("sql").append(currentSqlBuilder)
                        .append(".append(\"")
                        .append(removeSpecialChars(sql.substring(lastEnd, matcher.start())))
                        .append(" ?\");");
                sqlCodeBody.append(XmlDaoBeanCreator.SQL_OBJ_LIST_STR + ".add(").append(matcher.group(1)).append(");");

                lastEnd = matcher.end();
            }
        }
        sqlCodeBody.append("sql").append(currentSqlBuilder)
                .append(".append(\"").append(removeSpecialChars(sql.substring(lastEnd, sql.length()))).append("\");");
    }

    private String removeSpecialChars(String sql) {

        sql = sql.replace('\n', ' ');
        sql = sql.replace("\n", "\\n");
        sql = sql.replace("\'", "\\\'");
        sql = sql.replace("\"", "\\\"");
        return sql;
    }

    private void addMethod(String body) {
        try {
            if (SqlXmlPlugin.debug) {
                if (SqlXmlPlugin.debug) {
                    logger.info("method code:" + body);
                }
            }
            CtMethod method = CtMethod.make(body, theClass);
            theClass.addMethod(method);
        } catch (CannotCompileException ex) {
            throw new SqlXmlException("I don`t know !", ex);
        }
    }

    private void writeUpdateTypeCode() {
        codeBody.append("{");
        codeBody.append(sqlCodeBody);
        Class<?> returnType = iMethod.getReturnType();
        if (returnType == int.class || returnType == Integer.class) {
            codeBody.append("return com.jfinal.plugin.activerecord.Db.update(sql0.toString()," + XmlDaoBeanCreator.SQL_OBJ_LIST_STR + ".toArray());");
        } else if (Boolean.class.isAssignableFrom(returnType) || returnType == boolean.class) {
            codeBody.append("return (com.jfinal.plugin.activerecord.Db.update(sql0.toString()," + XmlDaoBeanCreator.SQL_OBJ_LIST_STR + ".toArray()))>0;");
        } else {
            throw new SqlXmlException("If you use update tag ,the  return type must be   int or boolean. ");
        }
        codeBody.append("}");
    }

    private void writeQueryTypeCode(mtype type) {
        codeBody.append("{");
        codeBody.append(sqlCodeBody);
        codeBody.append("return com.jfinal.plugin.activerecord.Db.").append(type.JFinalQueryName).append("(sql0.toString()," + XmlDaoBeanCreator.SQL_OBJ_LIST_STR + ".toArray());");
        codeBody.append("}");

    }

    private void writeQueryRecordTypeCode() {
        codeBody.append("{");
        codeBody.append(sqlCodeBody);
        codeBody.append("return com.jfinal.plugin.activerecord.Db.findFirst(sql0.toString()," + XmlDaoBeanCreator.SQL_OBJ_LIST_STR + ".toArray());");
        codeBody.append("}");

    }

    private void writeFindOneTypeCode() {
        codeBody.append("{");
        codeBody.append(sqlCodeBody);
        modelType = iMethod.getReturnType();
        String returnTypeName = modelType.getName();

        String daoFieldName = returnTypeName.replace('.', '_') + "INNERDAO";
        createOrGetFiled(daoFieldName);
        codeBody.append("return (").append(returnTypeName).append(")").append(daoFieldName).append(".findFirst(sql0.toString()," + XmlDaoBeanCreator.SQL_OBJ_LIST_STR + ".toArray());");
        codeBody.append("}");
    }

    private void writeListModelTypeCode() {
        codeBody.append("{");
        String daoFieldName = modelType.getName().replace('.', '_') + "INNERDAO";
        createOrGetFiled(daoFieldName);
        codeBody.append(sqlCodeBody);
        codeBody.append("return ")
                .append(daoFieldName).append(".find(sql0.toString()," + XmlDaoBeanCreator.SQL_OBJ_LIST_STR + ".toArray());");

        codeBody.append("}");

    }

    private void writeListRecordTypeCode() {

        codeBody.append("{");
        codeBody.append(sqlCodeBody);
        codeBody.append("return ")
                .append("com.jfinal.plugin.activerecord.Db.find(sql0.toString()," + XmlDaoBeanCreator.SQL_OBJ_LIST_STR + ".toArray());");

        codeBody.append("}");

    }

    private void writePageModelTypeCode() {
        codeBody.append("{");
        String daoFieldName = modelType.getName().replace('.', '_') + "INNERDAO";
        createOrGetFiled(daoFieldName);
        codeBody.append(sqlCodeBody);

        codeBody.append("return ")
                .append(daoFieldName).append(".paginate(")
                .append("pageNumber")
                .append(",")
                .append("pageSize")
                .append(",")
                .append("sql0.toString(),sql1.toString()," + XmlDaoBeanCreator.SQL_OBJ_LIST_STR + ".toArray());");
        codeBody.append("}");

    }

    private void writePageRecordTypeCode() {
        codeBody.append("{");
        codeBody.append(sqlCodeBody);

        codeBody.append("return ")
                .append("com.jfinal.plugin.activerecord.Db.paginate(\"")
                .append("pageNumber")
                .append(",")
                .append("pageSize")
                .append(",")
                .append("sql0.toString(),sql1.toString()," + XmlDaoBeanCreator.SQL_OBJ_LIST_STR + ".toArray());");
        codeBody.append("}");
    }

}
